package com.jiawa.train.business.service.impl;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.fastjson.JSON;
import com.jiawa.train.business.domain.po.ConfirmOrder;
import com.jiawa.train.business.domain.po.SkToken;
import com.jiawa.train.business.dto.ConfirmOrderMQDto;
import com.jiawa.train.business.enums.ConfirmOrderStatusEnum;
import com.jiawa.train.business.enums.RedisKeyPreEnum;
import com.jiawa.train.business.enums.RocketMQTopicEnum;
import com.jiawa.train.business.mapper.SkTokenMapper;
import com.jiawa.train.business.req.ConfirmOrderDoReq;
import com.jiawa.train.business.req.ConfirmOrderTicketReq;
import com.jiawa.train.business.service.IConfirmOrderService;
import com.jiawa.train.common.context.LoginMemberContext;
import com.jiawa.train.common.exception.BusinessException;
import com.jiawa.train.common.exception.BusinessExceptionEnum;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
@RequiredArgsConstructor
public class BeforeConfirmOrderService {
    private final IConfirmOrderService confirmOrderService;
    private final StringRedisTemplate redisTemplate;
    private final SkTokenMapper skTokenMapper;
    private final RedissonClient redissonClient;

    @Autowired
    RocketMQTemplate rocketMQTemplate;

    // @Resource
    // public RocketMQTemplate rocket
    // public RocketMQTemplate rocketMQTemplate;

    @SentinelResource(value = "beforeDoConfirm", blockHandler = "beforeDoConfirmBlock")
    public Long beforeDoConfirm(ConfirmOrderDoReq req) {
        // 根据前端传值，加入排队人数
        req.setMemberId(LoginMemberContext.getId());
        //2.校验令牌 - 加乐观锁
        boolean flag = validateToken(req.getDate()
                , req.getTrainCode()
                , LoginMemberContext.getId());
        if (flag) {
            log.info("成功获取令牌,可进行购买");
        } else {
            log.info("未获取到令牌,无法进行购买");
            throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_SK_TOKEN_FAIL);
        }
        Date date = req.getDate();
        String trainCode = req.getTrainCode();
        String start = req.getStart();
        String end = req.getEnd();
        List<ConfirmOrderTicketReq> tickets = req.getTickets();

        // 保存确认订单表，状态初始
        ConfirmOrder confirmOrder = new ConfirmOrder();
        confirmOrder.setCreateTime(LocalDateTime.now());
        confirmOrder.setUpdateTime(LocalDateTime.now());
        confirmOrder.setMemberId(req.getMemberId());
        confirmOrder.setDate(date);
        confirmOrder.setTrainCode(trainCode);
        confirmOrder.setStart(start);
        confirmOrder.setEnd(end);
        confirmOrder.setDailyTrainTicketId(req.getDailyTrainTicketId());
        confirmOrder.setStatus(ConfirmOrderStatusEnum.INIT.getCode());
        confirmOrder.setTickets(JSON.toJSONString(tickets));
        confirmOrderService.save(confirmOrder);

        // 发送MQ排队购票
        ConfirmOrderMQDto confirmOrderMQDto = new ConfirmOrderMQDto();
        confirmOrderMQDto.setDate(req.getDate());
        confirmOrderMQDto.setTrainCode(req.getTrainCode());
        confirmOrderMQDto.setLogId(MDC.get("LOG_ID")); //从日志上下文中获取日志ID并设置参数
        String reqJson = JSON.toJSONString(confirmOrderMQDto);
        log.info("排队购票，发送mq开始，消息：{}", reqJson);
        rocketMQTemplate.convertAndSend(RocketMQTopicEnum.CONFIRM_ORDER.getCode(), reqJson);
        log.info("排队购票，发送mq结束");
//        confirmOrderService.doConfirm(confirmOrderMQDto);
//        id = confirmOrder.getId();
        return confirmOrder.getId();
    }

    /**
     * 降级方法，需包含限流方法的所有参数和BlockException参数
     * @param req
     * @param e
     */
    public void beforeDoConfirmBlock(ConfirmOrderDoReq req, BlockException e) {
        log.info("购票请求被限流：{}", req);
        throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_FLOW_EXCEPTION);
    }


    private boolean validateToken(Date date, String trainCode, Long memberId) {
        RLock lock = null;
        String key = date + "-" + trainCode + memberId;
        lock = redissonClient.getLock(key);
        boolean trylock = false;
        try {
            //注意给锁加上5秒过时时间,防止人反复点击抢票
            trylock = lock.tryLock(0,5, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            log.info("获取令牌锁出现异常,key={}", key);
            return false;
        }
        if (trylock) {
            log.info("获取到令牌锁,key={}", key);
        } else {
            log.info("未获取到令牌锁,key={}", key);
            return false;
        }
        //获取到令牌锁后去获取令牌
        String Count_key = RedisKeyPreEnum.SK_TOKEN_COUNT.getCode() + date + "-" + trainCode;
        //先去缓存尝试获取令牌
        Object SKTokenCount = redisTemplate.opsForValue().get(Count_key);
        log.info("获取到的令牌数量={}", SKTokenCount);
        if(SKTokenCount != null){
            //说明缓存中存在这个结构
            Long count = redisTemplate.opsForValue().decrement(Count_key, 1);
            if(count < 0){
                //说明令牌已经被抢完了
                log.info("令牌已经被抢完了,key={}", Count_key);
                return false;
            }else{
                //说明令牌还未被抢完
                log.info("成功获取redis中令牌,key={}", Count_key);
                if(count % 10 ==0){
                    //每10个令牌更新一次到数据库
                    skTokenMapper.decrease(date, trainCode, 10);
                }
                return true;
            }
        }else{
            //缓存中没这个结构,先去数据库查询
            SkToken skToken = skTokenMapper.selectByTrainAndDate(trainCode, date);
            if (skToken == null) {
                //说明数据库中也没有这个结构
                log.info("数据库中也没有这个结构,key={}", Count_key);
                return false;
            }
            Integer count = skToken.getCount();
            if(count <= 0){
                //说明令牌已经被抢完了
                log.info("令牌已经被抢完了,key={}", Count_key);
                return false;
            }else{
                //说明令牌还未被抢完
                log.info("成功获取令牌,key={}", Count_key);
                //放置到缓存 - 更新缓存时间为60秒 - 这里先不更新数据库,由上面的10个令牌更新一次数据库
                redisTemplate.opsForValue().set(Count_key, (--count).toString());
                return true;
            }
        }
    }
}
